/*
    SUSAN® - Sole of Unix Save ANything

   Copyright (C) 2004-2008 Hechzh Property
      Copyright (C) 2011-2016 Skyatlas Co. LTD

   Copyright (C) 2013-2013 Hechzh Property

   
*/
/*
 * Susan authentication. Provides authentication with File and Storage daemons.
 *
 * Nicolas Boichat, August MMIV
 */

#include "monitoritem.h"
#include "authenticate.h"
#include "include/jcr.h"
#include "monitoritemthread.h"

#include "lib/tls_conf.h"
#include "lib/bsock.h"
#include "lib/bnet.h"
#include "lib/qualified_resource_name_type_converter.h"
#include "lib/bstringlist.h"
#include "lib/parse_conf.h"
#include "lib/util.h"

const int debuglevel = 50;

/* Commands sent to Storage daemon and File daemon and received
 *  from the User Agent */
static char SDFDhello[] = "Hello Director %s calling\n";

/* Response from SD */
static char SDOKhello[] = "3000 OK Hello\n";
/* Response from FD */
static char FDOKhello[] = "2000 OK Hello";

static std::map<AuthenticationResult, std::string>
    authentication_error_to_string_map{
        {AuthenticationResult::kNoError, "No Error"},
        {AuthenticationResult::kAlreadyAuthenticated, "Already authenticated"},
        {AuthenticationResult::kQualifiedResourceNameFailed,
         "Could not generate a qualified resource name"},
        {AuthenticationResult::kTlsHandshakeFailed, "TLS handshake failed"},
        {AuthenticationResult::kSendHelloMessageFailed,
         "Send of hello handshake message failed"},
        {AuthenticationResult::kCramMd5HandshakeFailed,
         "Challenge response handshake failed"},
        {AuthenticationResult::kDaemonResponseFailed,
         "Daemon response could not be read"},
        {AuthenticationResult::kRejectedByDaemon,
         "Authentication was rejected by the daemon"},
        {AuthenticationResult::kUnknownDaemon, "Unkown daemon type"}};

bool GetAuthenticationResultString(AuthenticationResult err,
                                   std::string& buffer)
{
  if (authentication_error_to_string_map.find(err) !=
      authentication_error_to_string_map.end()) {
    buffer = authentication_error_to_string_map.at(err);
    return true;
  }
  return false;
}

static AuthenticationResult AuthenticateWithDirector(JobControlRecord* jcr,
                                                     DirectorResource* dir_res)
{
  if (jcr->authenticated) {
    return AuthenticationResult::kAlreadyAuthenticated;
  }

  SusanSocket* dir = jcr->dir_bsock;
  MonitorResource* monitor = MonitorItemThread::instance()->getMonitor();
  if (dir_res->IsTlsConfigured()) {
    std::string qualified_resource_name;
    if (!my_config->GetQualifiedResourceNameTypeConverter()->ResourceToString(
            monitor->resource_name_, R_CONSOLE, qualified_resource_name)) {
      return AuthenticationResult::kQualifiedResourceNameFailed;
    }

    if (!dir->DoTlsHandshake(TlsPolicy::kBnetTlsAuto, dir_res, false,
                             qualified_resource_name.c_str(),
                             monitor->password.value, jcr)) {
      return AuthenticationResult::kTlsHandshakeFailed;
    }
  }

  uint32_t response_id;
  BStringList response_args;
  if (!dir->ConsoleAuthenticateWithDirector(
          jcr, monitor->resource_name_, monitor->password, dir_res,
          my_config->CreateOwnQualifiedNameForNetworkDump(), response_args,
          response_id)) {
    Jmsg(jcr, M_FATAL, 0,
         _("Director authorization problem.\n"
           "Most likely the passwords do not agree.\n"));
    return AuthenticationResult::kCramMd5HandshakeFailed;
  }

  return AuthenticationResult::kNoError;
}

static AuthenticationResult AuthenticateWithStorageDaemon(
    JobControlRecord* jcr,
    StorageResource* store)
{
  if (jcr->authenticated) {
    return AuthenticationResult::kAlreadyAuthenticated;
  }

  SusanSocket* sd = jcr->store_bsock;
  MonitorResource* monitor = MonitorItemThread::instance()->getMonitor();
  if (store->IsTlsConfigured()) {
    std::string qualified_resource_name;
    if (!my_config->GetQualifiedResourceNameTypeConverter()->ResourceToString(
            monitor->resource_name_, R_DIRECTOR, qualified_resource_name)) {
      return AuthenticationResult::kQualifiedResourceNameFailed;
    }

    if (!sd->DoTlsHandshake(TlsPolicy::kBnetTlsAuto, store, false,
                            qualified_resource_name.c_str(),
                            store->password.value, jcr)) {
      return AuthenticationResult::kTlsHandshakeFailed;
    }
  }

  /**
   * Send my name to the Storage daemon then do authentication
   */
  char dirname[MAX_NAME_LENGTH];
  bstrncpy(dirname, monitor->resource_name_, sizeof(dirname));
  BashSpaces(dirname);

  sd->InitBnetDump(
      my_config->CreateOwnQualifiedNameForNetworkDump());

  if (!sd->fsend(SDFDhello, dirname)) {
    Dmsg1(debuglevel, _("Error sending Hello to Storage daemon. ERR=%s\n"),
          BnetStrerror(sd));
    Jmsg(jcr, M_FATAL, 0, _("Error sending Hello to Storage daemon. ERR=%s\n"),
         BnetStrerror(sd));
    return AuthenticationResult::kSendHelloMessageFailed;
  }

  bool auth_success = sd->AuthenticateOutboundConnection(
      jcr, my_config->CreateOwnQualifiedNameForNetworkDump(), dirname,
      store->password, store);
  if (!auth_success) {
    Dmsg2(debuglevel,
          "Director unable to authenticate with Storage daemon at \"%s:%d\"\n",
          sd->host(), sd->port());
    Jmsg(jcr, M_FATAL, 0,
         _("Director unable to authenticate with Storage daemon at \"%s:%d\". "
           "Possible causes:\n"
           "Passwords or names not the same or\n"
           "TLS negotiation problem or\n"
           "Maximum Concurrent Jobs exceeded on the SD or\n"
           "SD networking messed up (restart daemon).\n"),
         sd->host(), sd->port());
    return AuthenticationResult::kCramMd5HandshakeFailed;
  }

  Dmsg1(116, ">stored: %s", sd->msg);
  if (sd->recv() <= 0) {
    Jmsg3(jcr, M_FATAL, 0,
          _("dir<stored: \"%s:%s\" bad response to Hello command: ERR=%s\n"),
          sd->who(), sd->host(), sd->bstrerror());
    return AuthenticationResult::kDaemonResponseFailed;
  }

  Dmsg1(110, "<stored: %s", sd->msg);
  if (!bstrncmp(sd->msg, SDOKhello, sizeof(SDOKhello))) {
    Dmsg0(debuglevel, _("Storage daemon rejected Hello command\n"));
    Jmsg2(jcr, M_FATAL, 0,
          _("Storage daemon at \"%s:%d\" rejected Hello command\n"), sd->host(),
          sd->port());
    return AuthenticationResult::kRejectedByDaemon;
  }

  return AuthenticationResult::kNoError;
}

static AuthenticationResult AuthenticateWithFileDaemon(JobControlRecord* jcr,
                                                       ClientResource* client)
{
  if (jcr->authenticated) {
    return AuthenticationResult::kAlreadyAuthenticated;
  }

  SusanSocket* fd = jcr->file_bsock;
  MonitorResource* monitor = MonitorItemThread::instance()->getMonitor();
  if (client->IsTlsConfigured()) {
    std::string qualified_resource_name;
    if (!my_config->GetQualifiedResourceNameTypeConverter()->ResourceToString(
            monitor->resource_name_, R_DIRECTOR, qualified_resource_name)) {
      return AuthenticationResult::kQualifiedResourceNameFailed;
    }

    if (!fd->DoTlsHandshake(TlsPolicy::kBnetTlsAuto, client, false,
                            qualified_resource_name.c_str(),
                            client->password.value, jcr)) {
      return AuthenticationResult::kTlsHandshakeFailed;
    }
  }

  /**
   * Send my name to the File daemon then do authentication
   */
  char dirname[MAX_NAME_LENGTH];
  bstrncpy(dirname, monitor->resource_name_, sizeof(dirname));
  BashSpaces(dirname);

  fd->InitBnetDump(
      my_config->CreateOwnQualifiedNameForNetworkDump());

  if (!fd->fsend(SDFDhello, dirname)) {
    Jmsg(jcr, M_FATAL, 0,
         _("Error sending Hello to File daemon at \"%s:%d\". ERR=%s\n"),
         fd->host(), fd->port(), fd->bstrerror());
    return AuthenticationResult::kSendHelloMessageFailed;
  }
  Dmsg1(debuglevel, "Sent: %s", fd->msg);

  bool auth_success = fd->AuthenticateOutboundConnection(
      jcr, my_config->CreateOwnQualifiedNameForNetworkDump(), dirname,
      client->password, client);

  if (!auth_success) {
    Dmsg2(debuglevel, "Unable to authenticate with File daemon at \"%s:%d\"\n",
          fd->host(), fd->port());
    Jmsg(jcr, M_FATAL, 0,
         _("Unable to authenticate with File daemon at \"%s:%d\". Possible "
           "causes:\n"
           "Passwords or names not the same or\n"
           "TLS negotiation failed or\n"
           "Maximum Concurrent Jobs exceeded on the FD or\n"
           "FD networking messed up (restart daemon).\n"),
         fd->host(), fd->port());
    return AuthenticationResult::kCramMd5HandshakeFailed;
  }

  Dmsg1(116, ">filed: %s", fd->msg);
  if (fd->recv() <= 0) {
    Dmsg1(debuglevel,
          _("Bad response from File daemon to Hello command: ERR=%s\n"),
          BnetStrerror(fd));
    Jmsg(jcr, M_FATAL, 0,
         _("Bad response from File daemon at \"%s:%d\" to Hello command: "
           "ERR=%s\n"),
         fd->host(), fd->port(), fd->bstrerror());
    return AuthenticationResult::kDaemonResponseFailed;
  }

  Dmsg1(110, "<filed: %s", fd->msg);
  if (strncmp(fd->msg, FDOKhello, sizeof(FDOKhello) - 1) != 0) {
    Jmsg(jcr, M_FATAL, 0, _("File daemon rejected Hello command\n"));
    return AuthenticationResult::kRejectedByDaemon;
  }

  return AuthenticationResult::kNoError;
}

AuthenticationResult AuthenticateWithDaemon(MonitorItem* item,
                                            JobControlRecord* jcr)
{
  switch (item->type()) {
    case R_DIRECTOR:
      return AuthenticateWithDirector(jcr, (DirectorResource*)item->resource());
    case R_CLIENT:
      return AuthenticateWithFileDaemon(jcr, (ClientResource*)item->resource());
    case R_STORAGE:
      return AuthenticateWithStorageDaemon(jcr,
                                           (StorageResource*)item->resource());
    default:
      printf(_("Error, currentitem is not a Client or a Storage..\n"));
      return AuthenticationResult::kUnknownDaemon;
  }
}
